/**
 * @Title: ThreeScene
 * @Author  dudalei
 * @Package
 * @Date  2024/11/7 17:38
 * @description:
 */
import {
  type Intersection,
  Object3D,
  type Object3DEventMap,
  PerspectiveCamera,
  Raycaster,
  Scene,
  Vector2,
  WebGLRenderer,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import ResourceTracker from '@/components/ThreeScene/common/ResourceTracker'
// 点击事件对象类型
export type intersectObjectsType = Intersection<Object3D<Object3DEventMap>>
// 点击事件类型
export type clickCallbackType = (e: intersectObjectsType[]) => void

/**
 * threejs场景
 */
export class ThreeScene {
  /**
   * 相机
   */
  public camera: PerspectiveCamera
  /**
   * 渲染器
   */
  public renderer: WebGLRenderer
  /**
   * 轨道控制器
   */
  public controls: OrbitControls
  /**
   * 场景
   */
  public scene: Scene
  // 点击事件回调数组
  public clickCallbacks: clickCallbackType[] = []

  /**
   * 光线投射器
   */
  raycaster: Raycaster
  /**
   * 鼠标坐标
   * @private
   */
  pointer: Vector2
  /**
   * 是否渲染结束
   * @private
   */
  private renderRequested = false
  private canvas: HTMLCanvasElement

  /**
   * 构造函数
   * @param canvas 要渲染的画布
   */
  constructor(canvas: HTMLCanvasElement) {
    this.canvas = canvas
    // 创建渲染器
    this.renderer = new WebGLRenderer({ antialias: true, canvas })

    // 创建相机
    const fov = 75
    const aspect = window.innerWidth / window.innerHeight
    const near = 0.1
    const far = 1000
    this.camera = new PerspectiveCamera(fov, aspect, near, far)

    this.camera.position.z = -70
    this.camera.position.y = 25
    this.camera.position.x = 90

    // 创建轨道控制器
    this.controls = new OrbitControls(this.camera, canvas)
    // 是否需要惯性
    // this.controls.enableDamping = true
    this.controls.target.set(0, 0, 0)
    this.controls.update()
    // 创建场景
    this.scene = new Scene()

    // 添加射线
    this.raycaster = new Raycaster()
    this.pointer = new Vector2()

    // 添加事件
    if (canvas.parentElement) {
      canvas.parentElement.onclick = this.onPointerDown
    }

    // 监听轨道控制器事件
    this.controls.addEventListener('change', this.requestRenderIfNotRequested)
    // 监听窗口大小变化
    window.addEventListener('resize', this.requestRenderIfNotRequested)
  }

  clickCallback(fn: (e: intersectObjectsType[]) => void) {
    this.clickCallbacks.push(fn)
  }
  onPointerDown = (event: MouseEvent) => {
    if (this.clickCallbacks.length === 0) {
      return
    }
    // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
    this.pointer.x = (event.clientX / window.innerWidth) * 2 - 1
    this.pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
    // 通过摄像机和鼠标位置更新射线
    this.raycaster.setFromCamera(this.pointer, this.camera)

    // 计算物体和射线的焦点
    const intersects: intersectObjectsType[] = this.raycaster.intersectObjects(this.scene.children)
    if (intersects.length > 0) {
      // 触发点击回调
      this.clickCallbacks.forEach((fn) => fn(intersects))
    }
  }

  /**
   * 渲染函数
   */
  render = () => {
    this.renderRequested = false

    // 调整渲染器大小
    if (this.resizeRendererToDisplaySize(this.renderer)) {
      const canvas = this.renderer.domElement
      this.camera.aspect = canvas.clientWidth / canvas.clientHeight
      this.camera.updateProjectionMatrix()
    }

    // 更新轨道控制器
    this.controls.update()
    // 渲染
    this.renderer.render(this.scene, this.camera)
  }

  /**
   * 请求渲染
   */
  requestRenderIfNotRequested = () => {
    if (!this.renderRequested) {
      this.renderRequested = true
      requestAnimationFrame(this.render)
    }
  }
  /**
   * 将渲染器大小调整为显示大小
   * @param renderer
   */
  resizeRendererToDisplaySize(renderer: WebGLRenderer) {
    const canvas = renderer.domElement
    const width = canvas.clientWidth
    const height = canvas.clientHeight
    const needResize = canvas.width !== width || canvas.height !== height
    if (needResize) {
      renderer.setSize(width, height, false)
    }

    return needResize
  }

  /**
   * 销毁
   */
  destroy() {
    window.removeEventListener('resize', this.requestRenderIfNotRequested)
    if (this.canvas.parentElement) {
      this.canvas.parentElement.onclick = null
    }
    const resMgr = new ResourceTracker()
    const track = resMgr.track.bind(resMgr)
    track(this.scene)
    // 需要删除的时候调用
    resMgr.dispose()
  }
}
